﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;
using Microsoft.CSharp;
using System.Reflection;
using System.CodeDom.Compiler;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;

namespace CodeScrapWindow
{
	internal sealed class CodeScrapControlViewModel : INotifyPropertyChanged
	{
		#region Events

		/// <summary>
		/// Occurs when a property value changes.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;

		#endregion Events

		#region Constants

		private const string AssemblyDirective = "@assembly";
		private const string ImportDirective = "@import";

		public const string CodePropertyName = "Code";
		public const string OutputPropertyName = "Output";
		public const string CompilerPropertyName = "Compiler";

		#endregion Constant

		#region Variables

		private string _code;
		private string _output;
		private string _compiler;

		#endregion Variables

		#region Properties

		/// <summary>
		/// Gets or sets the code.
		/// </summary>
		/// <value>The code.</value>
		public string Code
		{
			get
			{
				return _code;
			}
			set
			{
				if (_code != value)
				{
					_code = value;
					OnPropertyChanged(new PropertyChangedEventArgs(CodePropertyName));
				}
			}
		}

		/// <summary>
		/// Gets or sets the output.
		/// </summary>
		/// <value>The output.</value>
		public string Output
		{
			get
			{
				return _output;
			}
			set
			{
				if (_output != value)
				{
					_output = value;
					OnPropertyChanged(new PropertyChangedEventArgs(OutputPropertyName));
				}
			}
		}

		/// <summary>
		/// Gets or sets the compiler.
		/// </summary>
		/// <value>The compiler.</value>
		public string Compiler
		{
			get
			{
				return _compiler;
			}
			set
			{
				if (_compiler != value)
				{
					_compiler = value;
					OnPropertyChanged(new PropertyChangedEventArgs(CompilerPropertyName));
				}
			}
		}

		#endregion Properties

		#region Constructor

		/// <summary>
		/// Initializes a new instance of the <see cref="CodeScrapControlViewModel"/> class.
		/// </summary>
		public CodeScrapControlViewModel()
		{
			// Put some instructions in the output
			_code = @"";
			_output = @"Type code in the text box to the left.  Use System.Diagnostics.Debug.WriteLine("""") to print output here";
			_compiler = CSharpCompilerProvider.CompilerName;
		}

		#endregion Constructor

		#region Execute Command

		private RelayCommand _executeCommand;

		/// <summary>
		/// Gets the execute command.
		/// </summary>
		/// <value>The execute command.</value>
		public ICommand ExecuteCommand
		{
			get
			{
				if (_executeCommand == null)
				{
					_executeCommand = new RelayCommand(param => this.Execute());
				}
				return _executeCommand;
			}
		}

		/// <summary>
		/// Finds the previous.
		/// </summary>
		private void Execute()
		{
			try
			{
				this.Output = "";

				if (!string.IsNullOrEmpty(this.Code))
				{
					// Pre-process the code to break it up into chunks
					List<string> assemblies;
					List<string> imports;
					string code;
					PreProcessInput(out assemblies, out imports, out code);

					// Build the class code, compile it into an assembly and execute the generated method
					if (!string.IsNullOrEmpty(code))
					{
						string classCode = CompilerFactory.GetCompilerProvider(this.Compiler).BuildCode(imports, code);
						Assembly assembly = CreateAssembly(assemblies, classCode);
						if (assembly != null)
						{
							// Make a new trace listener to listen for debug statements and write them out
							GeneratedCodeTraceListener listener = new GeneratedCodeTraceListener();
							System.Diagnostics.Debug.Listeners.Add(listener);
							listener.MessageChanged += new EventHandler(Listener_MessageChanged);

							ExecuteMethod(assembly);

							// Don't need the listener anymore
							System.Diagnostics.Debug.Listeners.Remove(listener);
						}
					}
				}
			}
			catch (Exception ex)
			{
				Trace.WriteLine("CodeScrapControlViewModel:  " + ex.Message);
			}
		}

		private void Listener_MessageChanged(object sender, EventArgs e)
		{
			GeneratedCodeTraceListener listener = (GeneratedCodeTraceListener)sender;
			this.Output += listener.Message;
		}

		private void PreProcessInput(out List<string> assemblies, out List<string> imports, out string code)
		{
			assemblies = new List<string>();
			imports = new List<string>();
			code = "";

			try
			{
				int index;
				for (index = 0; index < this.Code.Length; index++)
				{
					// Ignore whitespace
					if (!char.IsWhiteSpace(this.Code, index))
					{
						// Check whether the next characters are a directive
						if (SafeSubstring(this.Code, index, CodeScrapControlViewModel.AssemblyDirective.Length) == CodeScrapControlViewModel.AssemblyDirective)
						{
							// Add the directive and skip past its characters
							int newLineIndex = this.Code.IndexOf(Environment.NewLine, index + 1);
							string directive = this.Code.Substring(index, newLineIndex - index);
							assemblies.Add(directive.Substring(CodeScrapControlViewModel.AssemblyDirective.Length).Trim());
							index += directive.Length;
						}
						else if (SafeSubstring(this.Code, index, CodeScrapControlViewModel.ImportDirective.Length) == CodeScrapControlViewModel.ImportDirective)
						{
							// Add the directive and skip past its characters
							int newLineIndex = this.Code.IndexOf(Environment.NewLine, index + 1);
							string directive = this.Code.Substring(index, newLineIndex - index);
							imports.Add(directive.Substring(CodeScrapControlViewModel.ImportDirective.Length).Trim());
							index += directive.Length;
						}
						else
						{
							// It's not a directive, it must be the start of the code!
							break;
						}
					}
				}

				// Add a reference to System.dll by default
				if (!assemblies.Contains("System.dll"))
				{
					assemblies.Add("System.dll");
				}

				// And a using statement for System
				if (!imports.Contains("System"))
				{
					imports.Add("System");
				}

				// The rest of it is code
				code = this.Code.Substring(index);
			}
			catch (Exception ex)
			{
				this.Output += string.Format("ERROR: {0}", ex.ToString()) + Environment.NewLine;
				Trace.WriteLine("PreProcessInput:  " + ex.Message);
			}
		}

		private Assembly CreateAssembly(List<string> assemblies, string classCode)
		{
			Assembly result = null;

			try
			{
				CodeDomProvider provider = CompilerFactory.GetCompilerProvider(this.Compiler).GetCompiler();

				// Make an in-memory assembly with debug information
				CompilerParameters parameters = new CompilerParameters();
				parameters.GenerateExecutable = false;
				parameters.GenerateInMemory = true;
				parameters.IncludeDebugInformation = true;
				parameters.TreatWarningsAsErrors = false;

				foreach (string assembly in assemblies)
				{
					parameters.ReferencedAssemblies.Add(assembly);
				}

				// Compile it
				CompilerResults compileResults = provider.CompileAssemblyFromSource(parameters, classCode);

				// Add any warnings or errors to the status message collection to be displayed to the user
				foreach (CompilerError error in compileResults.Errors)
				{
					if (error.IsWarning)
					{
						this.Output += error.ErrorText + Environment.NewLine;
					}
					else
					{
						this.Output += error.ErrorText + Environment.NewLine;
					}
				}

				if (!compileResults.Errors.HasErrors)
				{
					result = compileResults.CompiledAssembly;
				}
			}
			catch (Exception ex)
			{
				this.Output += string.Format("ERROR: {0}", ex.ToString()) + Environment.NewLine;
				Trace.WriteLine("CreateAssembly:  " + ex.Message);
			}

			return result;
		}

		private void ExecuteMethod(Assembly assembly)
		{
			try
			{
				Type t = assembly.GetType(string.Format("{0}.{1}", Constants.GeneratedNamespace, Constants.GeneratedClassName));
				if (t != null)
				{
					object classObject = Activator.CreateInstance(t);
					if (classObject != null)
					{
						MethodInfo method = classObject.GetType().GetMethod(Constants.GeneratedMethodName);
						if (method != null)
						{
							method.Invoke(classObject, null);
						}
					}
				}
			}
			catch (Exception ex)
			{
				this.Output += string.Format("ERROR: {0}", ex.ToString()) + Environment.NewLine;
				Trace.WriteLine("ExecuteMethod:  " + ex.Message);
			}
		}

		private string SafeSubstring(string input, int startIndex, int length)
		{
			if (startIndex >= 0 && input.Length > startIndex + length)
			{
				return input.Substring(startIndex, length);
			}
			else
			{
				return "";
			}
		}

		#endregion Execute Command

		#region Event Methods

		/// <summary>
		/// Raises the <see cref="E:PropertyChanged"/> event.
		/// </summary>
		/// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
		private void OnPropertyChanged(PropertyChangedEventArgs e)
		{
			PropertyChangedEventHandler changed = PropertyChanged;
			if (changed != null)
			{
				changed(this, e);
			}
		}

		#endregion Event Methods
	}
}
